﻿using codeparser.net.v2.Aspects;
using codeparser.net.v2.Entities;
using codeparser.net.v2.Exceptions;
using codeparser.net.v2.Validators;

using System;
using System.Collections.Generic;
using System.Globalization;

namespace codeparser.net.v2.Lexer
{
    /// <summary>
    /// Represents a lexer.
    /// </summary>
    internal class Lexer
    {
        /// <summary>
        /// Raised when the state has changed.
        /// </summary>
        internal event EventHandler<StateChangedEventArgs> StateChanged;

        /// <summary>
        /// Gets or sets the current state of the lexer.
        /// </summary>
        /// <value>The current state.</value>
        internal StateBase CurrentState
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the token validator strategy.
        /// </summary>
        /// <value>The token validator strategy.</value>
        internal TokenValidatorStrategyBase TokenValidatorStrategy
        {
            get;
            set;
        }

        /// <summary>
        /// Contains the current token.
        /// </summary>
        private Token _currentToken;

        /// <summary>
        /// Contains the list of tokens.
        /// </summary>
        private List<Token> _tokens;

        /// <summary>
        /// Contains the tag configuration.
        /// </summary>
        private TagConfiguration _tagConfiguration;

        /// <summary>
        /// Contains whether the lexer is enabled.
        /// </summary>
        private bool _isLexerEnabled = true;

        /// <summary>
        /// Contains the position where the lexer should be disabled.
        /// </summary>
        private int _disableLexerAtPosition;

        /// <summary>
        /// Contains the position where the lexer should be enabled.
        /// </summary>
        private int _enableLexerAtPosition;

        /// <summary>
        /// Initializes a new instance of the Lexer type.
        /// </summary>
        [LoggingAspect]
        internal Lexer(TagConfiguration tagConfiguration, TokenValidatorStrategy tokenValidatorStrategy)
        {
            // Initialize the fields.
            this.CurrentState = new StateText();
            this._currentToken = new Token();
            this._tokens = new List<Token>();
            this._tagConfiguration = tagConfiguration;
            this._disableLexerAtPosition = -1;
            switch (tokenValidatorStrategy)
            {
                case codeparser.net.v2.Validators.TokenValidatorStrategy.CaseSensitive:
                    this.TokenValidatorStrategy = new TokenValidatorCaseSensitiveStrategy();
                    break;
                case codeparser.net.v2.Validators.TokenValidatorStrategy.CaseInsensitive:
                    this.TokenValidatorStrategy = new TokenValidatorCaseInsensitiveStrategy();
                    break;
                default:
                    throw new UnknownTokenValidatorStrategyException("Unknown token validator strategy");
            }

            // Subscribe to events.
            this.StateChanged += Lexer_StateChanged;
        }

        /// <summary>
        /// Executed when a state has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The event args.</param>
        [LoggingAspect]
        private void Lexer_StateChanged(object sender, StateChangedEventArgs e)
        {
            // Set the token type.
            this._currentToken.Type = e.OldState.TokenType;

            // Check whether the token shall be added to the list of tokens. If not, skip the
            // current token and simply create a new one.
            if (this._currentToken.Value.Length == 0 ||
                this._currentToken.Type == TokenType.Assignment ||
                this._currentToken.Type == TokenType.AttributeValueBegin ||
                this._currentToken.Type == TokenType.AttributeValueEnd ||
                this._currentToken.Type == TokenType.LeftBracket ||
                this._currentToken.Type == TokenType.RightBracket ||
                this._currentToken.Type == TokenType.Space ||
                this._currentToken.Type == TokenType.Unknown)
            {
                // Create a new token.
                this._currentToken = new Token();

                // Return to the caller.
                return;
            }

            // Perform some additional checks on the token.
            this.TokenValidatorStrategy.Validate(this._currentToken, this._tagConfiguration);

            // Add the token to the list of tokens.
            this._tokens.Add(this._currentToken);

            // Check whether the content should be parsed.
            if (this._currentToken.Type == TokenType.Element &&
                this._currentToken.Tag.ClosingTagStyle == TagClosingStyle.Required &&
                this._currentToken.Tag.ContentStyle == TagContentStyle.NoParse &&
                this._currentToken.Value[0] != '/')
            {
                // Find the closing bracket for the current tag.
                int endOfOpeningTag =
                    e.Text.IndexOf(']', e.Position);

                // If no closing bracket could be found, throw an exception.
                if (endOfOpeningTag == -1)
                {
                    throw new InvalidCharacterException(String.Format(CultureInfo.CurrentCulture,
                        "Invalid character '{0}' at position {1} detected", e.Text[e.Position], e.Position));
                }

                // Disable the lexer.
                this._disableLexerAtPosition = endOfOpeningTag + 2;

                // Look for the corresponding closing tag.
                int positionOfClosingTag =
                    e.Text.IndexOf(
                        "[/" + this._currentToken.Tag.Element.Name + "]", endOfOpeningTag + 1, StringComparison.CurrentCultureIgnoreCase);

                // If the tag could not be found, throw an exception.
                if (positionOfClosingTag == -1)
                {
                    throw new MissingClosingTagException(String.Format(CultureInfo.CurrentCulture,
                        "Missing closing tag for '{0}'", this._currentToken.Tag.Element.Name));
                }

                // Set the position of the closing tag for re-enabling the lexer again.
                this._enableLexerAtPosition = positionOfClosingTag;
            }

            // Create a new token.
            this._currentToken = new Token();
        }

        /// <summary>
        /// Tokenizes the specified text.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns>A list of tokens.</returns>
        [LoggingAspect]
        internal IList<Token> Tokenize(string text)
        {
            // Validate the text parameter.
            if (text == null)
            {
                throw new ArgumentNullException("text");
            }

            // Iterate over the text.
            int position = 0;
            foreach (char c in text)
            {
                // Check whether the lexer should be disabled. If so, disable it.
                if (position == this._disableLexerAtPosition)
                {
                    this._isLexerEnabled = false;
                }

                // Check if the lexer is disabled.
                if (!this._isLexerEnabled)
                {
                    // If so, check whether the closing tag has been reached. If not, move to the
                    // next position and skip the current iteration.
                    if (position < this._enableLexerAtPosition)
                    {
                        this._currentToken.Value += c;
                        this.CurrentState = new StateText();
                        position++;
                        continue;
                    }

                    // If the closing tag has been reached, cut the trailing bracket from the
                    // current token's value and enable the lexer again.
                    //this._currentToken.Value = this._currentToken.Value.Substring(1);
                    this._isLexerEnabled = true;
                }

                // Remember the current state.
                StateBase oldState = this.CurrentState;

                // Decide what to do on the current character.
                try
                {
                    switch (c)
                    {
                        case '[':
                            this.CurrentState.ProcessLeftBracket(this);
                            break;
                        case ']':
                            this.CurrentState.ProcessRightBracket(this);
                            break;
                        case '=':
                            this.CurrentState.ProcessAssignment(this);
                            break;
                        case '"':
                            this.CurrentState.ProcessQuotationMark(this);
                            break;
                        case ' ':
                            this.CurrentState.ProcessSpace(this);
                            break;
                        case '\n':
                            this.CurrentState.ProcessNewLine(this);
                            break;
                        default:
                            this.CurrentState.ProcessCharacter(this);
                            break;
                    }
                }
                catch (InvalidCharacterException e)
                {
                    throw new InvalidCharacterException(String.Format(CultureInfo.CurrentCulture,
                        "Invalid character '{0}' at position {1} detected", c, position), e);
                }

                // Check whether the state has changed, and if so, raise an event.
                if (oldState != this.CurrentState)
                {
                    // This adds the current token to the list of tokens and creates a new
                    // current token.
                    this.OnStateChanged(oldState, this.CurrentState, text, position);
                }

                // Add the current character to the current token.
                this._currentToken.Value += c;

                // Move to the next position.
                position++;
            }

            // Add the last token since this has not been done yet.
            OnStateChanged(this.CurrentState, null, text, position);

            // Compact the token list.
            this.Compact();

            // Return the list of tokens to the caller.
            return this._tokens;
        }

        /// <summary>
        /// Compacts the list of tokens.
        /// </summary>
        [LoggingAspect]
        private void Compact()
        {
            // Create a list of tokens.
            List<Token> result = new List<Token>();

            // Iterate over the original list.
            foreach (Token token in this._tokens)
            {
                // If there is a non-text token, add it to the result.
                if (token.Type != TokenType.Text)
                {
                    result.Add(token);
                    continue;
                }

                // If there is a text-token, add it to the result, if there is not yet a text
                // token, otherwise append it to that token.
                int indexOfLastAddedToken = result.Count - 1;
                if (indexOfLastAddedToken >= 0 && result[indexOfLastAddedToken].Type == TokenType.Text)
                {
                    result[indexOfLastAddedToken].Value += token.Value;
                }
                else
                {
                    result.Add(token);
                }
            }

            // Return the result to the caller.
            this._tokens = result;
        }

        /// <summary>
        /// Raises the OnStateChanged event.
        /// </summary>
        /// <param name="oldState">The old state.</param>
        /// <param name="newState">The new state.</param>
        /// <param name="text">The text.</param>
        /// <param name="position">The position.</param>
        [LoggingAspect]
        protected virtual void OnStateChanged(StateBase oldState, StateBase newState, string text, int position)
        {
            if (this.StateChanged != null)
            {
                this.StateChanged(this, new StateChangedEventArgs(oldState, newState, text, position));
            }
        }
    }
}